package org.aspectsense.middleware.scheduler;


import java.util.*;

/**
 * This class is used by the scheduler. It provides a linked list of sorted
 * timestamps, each one of which points to a set of runnables. This data
 * structure corresponds to the state of the scheduled list of events. As a
 * timestamp might imply the execution of multiple {@link Runnable}s, each item
 * in the list points to a set of runnables, rather than an individual
 * {@link Runnable}.
 *
 * @author Nearchos Paspallis [nearchos@cs.ucy.ac.cy]
 *         Date: Feb 8, 2008
 *         Time: 10:25:17 AM
 */
class ScheduledRunnables
{
    /**
     * A linked list of timestamp objects, which is always maintained sorted.
     */
    private final List<Long> sortedList = new LinkedList<Long>();

    /**
     * A map of Long values to Sets of {@link Runnable}s
     */
    private final Map<Long, Set> eventSets = new HashMap<Long, Set>();

    /**
     * Schedules the specified {@link Runnable} (enclosed in the {@link Event}
     * for execution. The minimum time before execution is specified by the
     * minWaitTime parameter
     * ({@link org.istmusic.mw.context.util.scheduler.Event#getMinWaitTime()})
     * in milliseconds. There are no guarantees as to the max wait time.
     *
     * @param event the {@link org.istmusic.mw.context.util.scheduler.Event} to
     * be scheduled
     */
    synchronized void addScheduledEvent(final Event event)
    {
        final long currentTimeMillis = System.currentTimeMillis();
        final long timestamp = currentTimeMillis + event.getMinWaitTime();

        addScheduledEvent(event, timestamp);
    }

    private void addScheduledEvent(final Event event, final long timestamp)
    {
        // update sorted list
        addToSortedList(timestamp);

        // update map
        Set<Event> events = (Set<Event>) eventSets.get(new Long(timestamp));
        if(events == null)
        {
            events = new HashSet<Event>();
            eventSets.put(new Long(timestamp), events);
        }
        events.add(event);
    }

    /**
     * Removes the specified {@link RecurringEvent} from the execution queue.
     *
     * @param event the {@link RecurringEvent} to be removed from the execution
     * queue.
     */
    synchronized void removeScheduledEvent(final RecurringEvent event)
    {
        /* Remove the first instance of the specified event (it is assumed
         * that only one instance of each event is present in the queue).
         */
        final Iterator<Long> iterator = sortedList.iterator();
        while(iterator.hasNext())
        {
            final Object timestamp = iterator.next();
            final Set<?> eventSet = (Set<?>) eventSets.get(timestamp);
            if(eventSet.contains(event))
            {
                eventSet.remove(event);
                // if this was the last event in the set, then also remove the
                // timestamp and the corresponding set
                if(eventSet.isEmpty())
                {
                    eventSets.remove(eventSet);
                    iterator.remove();
                }
                break;
            }
        }
    }

    /**
     * Retrieves (and removes if needed) the next scheduled runnable. This
     * method succeeds if and only if there is at least a scheduled item with
     * designated start timestamp before the specified
     * <i>earlierStartTimeMillis</i>.
     *
     * @param earlierStartTimeMillis the earliest start time acceptable for a
     * scheduled event; if the first scheduled event has a start time <i>after
     * </i> this parameter, then the <i>null</i> value is returned.
     * @return the next scheduled event, or <i>null</i> if there is no
     * scheduled event or the next scheduled event is designated to start after
     * the specified <i>earlierStartTimeMillis</i>.
     */
    synchronized Runnable popNextRunnable(final long earlierStartTimeMillis)
    {
        if(sortedList.isEmpty())
        {
            return null;
        }

        final Long timestamp = (Long) sortedList.get(0);
        if(timestamp.longValue() <= earlierStartTimeMillis)
        {
            final Set<?> set = (Set<?>) eventSets.get(timestamp);
            final Event event = (Event) set.iterator().next();

            set.remove(event);
            // check if the event is recurring and if so, add it back to the
            // queue of scheduled events again
            if(event instanceof RecurringEvent)
            {
                final RecurringEvent recurringEvent = (RecurringEvent) event;
                final long currentTimeMillis = System.currentTimeMillis();
                final long newTimestamp
                        = currentTimeMillis + recurringEvent.getInterval();
                addScheduledEvent(event, newTimestamp);
            }

            if(set.isEmpty())
            {
                eventSets.remove(timestamp);
                sortedList.remove(timestamp);
            }

            return event.getRunnable();
        }
        else // no scheduled event is mature yet, for the earlierStartTimeMillis
        {
            return null;
        }
    }

    private void addToSortedList(final long timestamp)
    {
        final int sizeOfList = sortedList.size();

        for(int i = 0; i < sizeOfList; i++)
        {
            final long previousTimestamp
                    = (i == 0) ? 0 : ((Long) sortedList.get(i - 1)).longValue();
            final long currentTimestamp
                    = ((Long) sortedList.get(i)).longValue();

            // ignore existing timestamps
            if(timestamp == currentTimestamp) return;

            if(previousTimestamp < timestamp && timestamp < currentTimestamp)
            {
                sortedList.add(i, new Long(timestamp));
                return;
            }
        }

        // at this point, we exhausted all points in list, and we add the
        // specified scheduledTimeMillis to the end of the list
        sortedList.add(sizeOfList, new Long(timestamp));
    }

    public int getNumberOfScheduledRunnables()
    {
        int numberOfScheduledRunnables = 0;

        final Iterator<Long> iterator = sortedList.iterator();
        while(iterator.hasNext())
        {
            final Set<?> set = (Set<?>) eventSets.get(iterator.next());
            numberOfScheduledRunnables += set.size();
        }

        return numberOfScheduledRunnables;
    }

    public String toString()
    {
        final StringBuffer stringBuffer
                = new StringBuffer("ScheduledRunnables:\n");

        final Iterator<Long> iterator = sortedList.iterator();
        while(iterator.hasNext())
        {
            final Long timestamp = (Long) iterator.next();
            final Set<?> runnables = (Set<?>) eventSets.get(timestamp);
            final int numOfRunnables = runnables.size();
            final Object [] runnablesArray
                    = runnables.toArray(new Object [numOfRunnables]);

            stringBuffer.append(timestamp).append(" -> {");
            for(int i = 0; i < numOfRunnables; i++)
            {
                stringBuffer.append(runnablesArray[i]);
                if(i < numOfRunnables - 1)
                {
                    stringBuffer.append(", ");
                }
            }
            stringBuffer.append("}\n");
        }

        return stringBuffer.toString();
    }
}